/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daiosPlugins.axis2.axiom;

import java.io.IOException;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.apache.axiom.om.OMDataSource;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.commons.collections.iterators.ArrayIterator;

import at.ac.tuwien.infosys.dsg.daios.exceptions.TypeErrorException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.UnsupportedClassException;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosInputMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosOutputMessage;
import at.ac.tuwien.infosys.dsg.daios.util.Utility;
import at.ac.tuwien.infosys.dsg.daios.wsdl.AtomicTypesMapper;
import at.ac.tuwien.infosys.dsg.daios.wsdl.WSDLEncoding;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLOperation;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLType;
import at.ac.tuwien.infosys.dsg.daios.wsdl.typesystem.WSDLTypeTree;

/**
 * The converter used for converting Daios messages to OM XML models in
 * case of SOAP invocations through the Axis2 backend.
 * 
 * <p>
 * 
 * This is an abstract root converter. There are concrete subclasses of
 * this converter for 'document' and 'rpc' styles. The correct implementation
 * can be accessed through OMElementConverter.getInstance() .
 * 
 * @author philipp
 *
 */
public abstract class OMElementConverter {
	
	protected Logger log = null;
	protected OMFactory fac = null;
	protected StringBuilder sb  = null;
	protected OMDataSource ds = null;
	
	/**
	 * This is the factory method for the subclasses implementing the
	 * conversion in case of 'document' or 'rpc' style.
	 * 
	 * @param style The style of the Web Service invocation ('rpc' or
	 * 'document').
	 * @param output The output message associated with your invocation.
	 * @param types The WSDL type tree from the WSDL definition.
	 * @return The a new converter instance for 'style'.
	 */
	public static OMElementConverter getInstance(WSDLEncoding style,
			IWSDLMessage output, WSDLTypeTree types) {
		
		// the RPC converter is default
		if(style == null)
			return OMRPCConverter.getInstance();
		
		if(style.equals(new WSDLEncoding(
				IConstants.RPC_STYLE, IConstants.ENCODED_STYLE)))
			return OMRPCConverter.getInstance();
		
		if(style.equals(new WSDLEncoding(
				IConstants.DOC_STYLE, IConstants.WRAPPED_STYLE)))
			return OMDocWrappedConverter.getInstance(output, types);
			
//		if(style.equals(new WSDLEncoding(
//				IConstants.DOC_STYLE, IConstants.LITERAL_STYLE)))
//			return OMDocLitConverter.getInstance(output, types);			
		
		return null;
		
	}
	
	/**
	 * Converts a XML response encoded as AXIOM model
	 * into a DaiosOutputMessage according to the
	 * output message associated with this converter.
	 * 
	 * @param returnMsg The XML response as OM model.
	 * @return The XML document converted to a DaiosOutputMessage.
	 */
	public abstract DaiosOutputMessage convert(OMElement returnMsg);
	
	/**
	 * Converts a Daios message to AXIOM using the given
	 * WSDL operation and WSDL input message.
	 * 
	 * @param op The WSDL operation to use in the invocation.
	 * @param msg The WSDL message to use in the invocation.
	 * @param actualMsg The Daios message that provides the
	 * acutal user-delivered input data.
	 * @param types A tree containing all types from the WSDL
	 * definition.
	 */
	public abstract OMElement convert(IWSDLOperation op, IWSDLMessage msg,
			DaiosInputMessage actualMsg, WSDLTypeTree types);
	
	/**
	 * Convert an asynchronously received document into a Daios message.
	 * For some reason we sometimes receive different messages from
	 * Axis2 depending on whether we use synchronous or asynchronous
	 * invocations. This might be a bug in Axis2.
	 * 
	 * @param result The asynchronous result to convert
	 * @return The converted Daios message.
	 */
	public abstract DaiosOutputMessage convertAsync(OMElement result);
	
	/**
	 * Create an empty OM element.
	 * 
	 * @param partName The name of the new OM element.
	 * @param uri The namespace URI of the new OM element.
	 * @param prefix The prefix of the new OM element.
	 * @return The newly created OM element.
	 * @throws XMLStreamException 
	 * @ 
	 */
	protected OMElement addEmptyOMPart(String partName, String uri,
			String prefix) {
		
//		if(uri != null) {
//			writeStartElement(partName,uri,prefix, xmlWriter);
//			writeEndElement(partName,prefix, xmlWriter);
//		}
//		else {
//			writeStartElement(partName, xmlWriter);
//			writeEndElement(partName, xmlWriter);
//		}
		
		OMNamespace ns = fac.createOMNamespace(uri, prefix);
		OMElement el = fac.createOMElement(partName, ns);
		return el;
		
	}
	
	/**
	 * Create an OM element with text content.
	 * 
	 * @param partName The name of the new OM element.
	 * @param value The text content of the new OM element.
	 * @param uri The namespace URI of the new OM element.
	 * @param prefix The prefix of the new OM element.
	 * @return The newly created OM element.
	 * @ 
	 */
	protected OMElement addOMPart(String partName, String value,
			String uri, String prefix) {
		
//		if(uri != null) {
//			writeStartElement(partName,uri,prefix, xmlWriter);
//			writeText(value, xmlWriter);
//			writeEndElement(partName,prefix, xmlWriter);
//		}
//		else {
//			writeStartElement(partName, xmlWriter);
//			writeText(value, xmlWriter);
//			writeEndElement(partName, xmlWriter);
//		}
		
		
		OMElement part = null;
		if(uri != null)
			part = fac.createOMElement(partName,
					fac.createOMNamespace(uri, prefix));
		else
			part = fac.createOMElement(partName, null);
		
		part.addChild(fac.createOMText(part, value));
		
		return part;
	
	}
	
	/**
	 * Create an OM element that contains an array of
	 * simple values.
	 * 
	 * @param partName The name of the new OM element.
	 * @param value An array of values.
	 * @param uri The namespace URI of the new OM element.
	 * @param prefix The prefix of the new OM element.
	 * @return The newly created OM element.
	 * @ 
	 */
	protected OMElement addOMPartFromArray(String partName, Object value,
			String uri, String prefix) {
		
//		if(uri != null) {
//			writeStartElement(partName,uri,prefix, xmlWriter);
//		}
//		else {
//			writeStartElement(partName, xmlWriter);
//		}
//		
//		ArrayIterator it = new ArrayIterator(value);
//		
//		String arraytype = 
//			AtomicTypesMapper.getArray(value.getClass());
//		
//		while(it.hasNext()) {
//			writeStartElement(arraytype, IConstants.XSD_NS, "xsd", xmlWriter);
//			writeText(AtomicTypesMapper.encodeForWire(it.next()), xmlWriter);
//			writeEndElement(arraytype, "xsd", xmlWriter);
//		}
//		
//		if(uri != null) {
//			writeEndElement(partName,prefix, xmlWriter);
//		}
//		else {
//			writeEndElement(partName, xmlWriter);
//		}
		
		OMElement part = null;
		if(uri != null)
			part = fac.createOMElement(partName,
					fac.createOMNamespace(uri, prefix));
		else
			part = fac.createOMElement(partName, null);
		
		ArrayIterator it = new ArrayIterator(value);
		
		String arraytype = 
			AtomicTypesMapper.getArray(value.getClass());
		
		OMElement newNode = null;
		while(it.hasNext()) {
			newNode = fac.createOMElement
				(new QName(IConstants.XSD_NS, arraytype),null);
			newNode.addChild(fac.createOMText(part,it.next().toString()));
			part.addChild(newNode);
		}
		
		return part;
	
	}
	
	/**
	 * Create an OM element that contains an array of
	 * simple values.
	 * 
	 * <p>
	 * 
	 * This method is equivalent to addOMPartFromArray(),
	 * with the subtle difference that this method returns
	 * a list of all array elements, while addOMPartFromArray()
	 * returns a single OM element with the array elements as 
	 * children.
	 * 
	 * @param partName The name of the new OM element.
	 * @param uri The namespace URI of the new OM element.
	 * @param prefix The prefix of the new OM element.
	 * @return The OM element containing the array of complex subelements.
	 * @ 
	 */
	protected List<OMElement> addOMPartFromArrayWithoutSubelement(
			String partName, Object value, String uri, String prefix) {
		
//		ArrayIterator it = new ArrayIterator(value);
//		
//		String arraytype = 
//			AtomicTypesMapper.getArray(value.getClass());
//		
//		while(it.hasNext()) {
//			writeStartElement(arraytype, IConstants.XSD_NS, "xsd", xmlWriter);
//			writeText(AtomicTypesMapper.encodeForWire(it.next()), xmlWriter);
//			writeEndElement(arraytype, "xsd", xmlWriter);
//		}
		
		ArrayIterator it = new ArrayIterator(value);
		List<OMElement> vals = new LinkedList<OMElement>();
		
		OMElement newNode = null;
		while(it.hasNext()) {
			newNode = fac.createOMElement(partName,
					fac.createOMNamespace(uri, prefix));
			newNode.addChild(fac.createOMText(newNode,
					AtomicTypesMapper.encodeForWire(it.next())));
			vals.add(newNode);
		}
		
		return vals;
		
	}
	
	/**
	 * Create an OM element that contains a complex 
	 * subelement.
	 * 
	 * @param partName The name of the new OM element.
	 * @param complex The complex type to encode as OM element.
	 * @param uri The namespace URI of the new OM element.
	 * @param prefix The prefix of the new OM element.
	 * @return The OM element containing the complex subelement.
	 * @ 
	 */	
	protected OMElement addOMPart(String partName, DaiosMessage complex,
			String uri, String prefix) {
		
//		if(uri != null) {
//			writeStartElement(partName,uri,prefix, xmlWriter);
//		}
//		else {
//			writeStartElement(partName, xmlWriter);
//		}
//		
//		Enumeration<String> names = complex.getNames();
//		String name = null;
//		while(names.hasMoreElements()) {
//			name = names.nextElement();
//			
//			boolean isArray = complex.isArray(name);
//			boolean isSimple = complex.isSimple(name);
//			
//			// is this a simple field ?
//			if(!isArray && isSimple)
//				addOMPart(name,
//					AtomicTypesMapper.encodeForWire(complex.get(name)),
//					uri, prefix, xmlWriter);
//			// or an array?
//			else if(isArray && isSimple) {
//				addOMPartFromArrayWithoutSubelement(name,
//						complex.get(name),uri, prefix, xmlWriter);
//			}
//			// ... or a complex field ?
//			else if(!isArray && !isSimple) {
//				try {
//					addOMPart(name, complex.getComplex(name),
//							uri, prefix, xmlWriter);
//				} catch (TypeErrorException e) {
//					log.severe("Internal Daios error while processing "+
//					"your request.");
//					e.printStackTrace();
//				}
//			}
//			// ... or an array of complex fields ?
//			else
//				try {
//					addOMPartFromArrayWithoutSubelement(name,
//							complex.getComplexArray(name),uri, prefix, xmlWriter);
//				} catch (TypeErrorException e) {
//					log.severe("Internal Daios error while processing "+
//					"your request.");
//					e.printStackTrace();
//				}			
//		}
//		
//		if(uri != null) {
//			writeEndElement(partName,prefix, xmlWriter);
//		}
//		else {
//			writeEndElement(partName, xmlWriter);
//		}
		
		OMElement part = null;
		if(uri != null)
			part = fac.createOMElement(partName,
					fac.createOMNamespace(uri, prefix));
		else
			part = fac.createOMElement(partName, null);
		
		Enumeration<String> names = complex.getNames();
		String name = null;
		while(names.hasMoreElements()) {
			name = names.nextElement();
			
			boolean isArray = complex.isArray(name);
			boolean isSimple = complex.isSimple(name);
			
			// is this a simple field ?
			if(!isArray && isSimple)
				part.addChild(addOMPart(name,
					AtomicTypesMapper.encodeForWire(complex.get(name)),
					uri, prefix));
			// or an array?
			else if(isArray && isSimple) {
				OMUtil.addAllChilds(part,
						addOMPartFromArrayWithoutSubelement(name,
							complex.get(name),uri, prefix));
			}
			// ... or a complex field ?
			else if(!isArray && !isSimple) {
				try {
					part.addChild(addOMPart(name, complex.getComplex(name),
							uri, prefix));
				} catch (TypeErrorException e) {
					log.severe("Internal Daios error while processing "+
					"your request.");
					e.printStackTrace();
				}
			}
			// ... or an array of complex fields ?
			else
				try {
					OMUtil.addAllChilds(part,
							addOMPartFromArrayWithoutSubelement(name,
								complex.getComplexArray(name),uri, prefix));
				} catch (TypeErrorException e) {
					log.severe("Internal Daios error while processing "+
					"your request.");
					e.printStackTrace();
				}			
		}
		
		return part;
	}	

	/**
	 * Create an OM element that contains a complex 
	 * subelement.
	 * 
	 * <p>
	 * 
	 * This method is similar to addOMPart(), with the
	 * difference that this time the complex type is
	 * encoded in accordance with a given WSDL type.
	 * 
	 * @param partName The name of the new OM element.
	 * @param complex The complex type to encode as OM element.
	 * @param uri The namespace URI of the new OM element.
	 * @param prefix The prefix of the new OM element.
	 * @param wrapper Format the OM element in accordance
	 * with this WSDL type.
	 * @return The OM element containing the complex subelement.
	 */	
	protected OMElement addOMPart(String partName, DaiosMessage complex,
			String uri, String prefix, IWSDLType wrapper) {
		
//		if(uri != null) {
//			writeStartElement(partName,uri,prefix, xmlWriter);
//		}
//		else {
//			writeStartElement(partName, xmlWriter);
//		}
//		
//		IWSDLType[] seq = wrapper.getSequence();
//		
//		String name = null;
//		boolean isArray = false;
//		boolean isSimple = false;
//		for(IWSDLType t: seq) {
//			name = t.getName().getLocalPart();
//			if(complex.get(name) == null) {
//				addEmptyOMPart(
//					name, uri, prefix, xmlWriter);
//				continue;
//			}
//			isArray = complex.isArray(name);
//			isSimple = complex.isSimple(name);
//			// is this a simple field ?
//			if(!isArray && isSimple)
//				addOMPart(name,
//					AtomicTypesMapper.encodeForWire(complex.get(name)),
//					uri, prefix, xmlWriter);
//			// or an array?
//			else if(isArray && isSimple)
//				addOMPartFromArrayWithoutSubelement(name,
//					complex.get(name),uri, prefix, xmlWriter);			
//			// ... or a complex field ?
//			else if(!isArray && !isSimple)
//				try {
//					addOMPart(name, complex.getComplex(name),
//							uri, prefix, xmlWriter);
//				} catch (TypeErrorException e) {
//					e.printStackTrace();
//				}
//			// ... or an array of complex fields?
//			else 
//				try {
//					addOMPart(name,
//						complex.getComplexArray(name), uri, prefix, xmlWriter);
//				} catch (TypeErrorException e) {
//					e.printStackTrace();
//				}				
//		}
//		
//		if(uri != null) {
//			writeEndElement(partName,prefix, xmlWriter);
//		}
//		else {
//			writeEndElement(partName, xmlWriter);
//		}
		
		OMElement part = fac.createOMElement(partName,
			fac.createOMNamespace(uri, prefix));
		
		IWSDLType[] seq = wrapper.getSequence();
		
		String name = null;
		boolean isArray = false;
		boolean isSimple = false;
		for(IWSDLType t: seq) {
			name = t.getName().getLocalPart();
			if(complex.get(name) == null) {
				part.addChild(addEmptyOMPart(
					name, uri, prefix));
				continue;
			}
			isArray = complex.isArray(name);
			isSimple = complex.isSimple(name);
			// is this a simple field ?
			if(!isArray && isSimple)
				part.addChild(addOMPart(name,
					AtomicTypesMapper.encodeForWire(complex.get(name)),
					uri, prefix));
			// or an array?
			else if(isArray && isSimple)
				OMUtil.addAllChilds(part, addOMPartFromArrayWithoutSubelement(name,
						complex.get(name),uri, prefix));			
			// ... or a complex field ?
			else if(!isArray && !isSimple)
				try {
					part.addChild(addOMPart(name, complex.getComplex(name),
							uri, prefix));
				} catch (TypeErrorException e) {
					e.printStackTrace();
				}
			// ... or an array of complex fields?
			else 
				try {
					OMUtil.addAllChilds(part,addOMPart(name,
							complex.getComplexArray(name), uri, prefix));
				} catch (TypeErrorException e) {
					e.printStackTrace();
				}				
		}
		
		return part;
	}
	
	/**
	 * Create an OM element that contains an array of complex 
	 * subelements.
	 * 
	 * @param partName The name of the new OM element.
	 * @param complexArray The complex array to encode as OM element.
	 * @param uri The namespace URI of the new OM element.
	 * @param prefix The prefix of the new OM element.
	 * @return The array of complex subelements.
	 * @ 
	 */	
	protected List<OMElement> addOMPart(String partName,
			DaiosMessage[] complexArray, String uri, String prefix) {
		
//		if(uri != null) {
//			writeStartElement(partName,uri,prefix, xmlWriter);
//		}
//		else {
//			writeStartElement(partName, xmlWriter);
//		}
//		
//		for(DaiosMessage m : complexArray)
//			addOMPart(partName, m, uri, prefix, xmlWriter);
//		
//		if(uri != null) {
//			writeEndElement(partName,prefix, xmlWriter);
//		}
//		else {
//			writeEndElement(partName, xmlWriter);
//		}
		
		
		List<OMElement> vals = new LinkedList<OMElement>();
		
		OMElement newNode = null;
		for(DaiosMessage m : complexArray) {
			newNode = addOMPart(partName, m, uri, prefix);
			vals.add(newNode);
		}
		
		return vals;
		
	}		
	
	/**
	 * Convert a list of Strings into an array of a certain type.
	 * 
	 * @param l The list of Strings.
	 * @param arrayType The target array type, i.e. 'int'.
	 * @return The newly created array as java.lang.Object
	 */
	protected Object convert(List<String> l, String arrayType) {
		
		try {
			
			return Utility.convert(l, arrayType);
			
		} catch(IOException e) {
			log.severe("Internal Daios error while processing "+
				"your request.");
			e.printStackTrace();
		} catch (UnsupportedClassException e) {
			log.severe("Internal Daios error while processing "+
				"your request.");
			e.printStackTrace();
		}
		
		return null;
		
	}
	
	/**
	 * Convert a list of Daios messages into an array of Daios messages.
	 * 
	 * @param l The list of messages.
	 * @return The new array.
	 */
	protected DaiosMessage[] convert(List<DaiosMessage> l) {
		
		return Utility.convert(l);
		
	}
	
	private void writeStartElement(String partName, String uri,
			String prefix, XMLStreamWriter xmlWriter) throws XMLStreamException  {
		
        if (uri != null) {
            java.lang.String writerPrefix = xmlWriter.getPrefix(uri);
            if (writerPrefix != null) {
                xmlWriter.writeStartElement(uri, partName);
            } else {
                if (prefix == null) {
                    prefix = "ns";//org.apache.axis2.databinding.utils.BeanUtil.getUniquePrefix();
                }

                xmlWriter.writeStartElement(prefix, partName, uri);
                xmlWriter.writeNamespace(prefix, uri);
                xmlWriter.setPrefix(prefix, uri);
            }
        } else {
    		xmlWriter.writeStartElement(partName);
        }
		
	}
	
	private void writeStartElement(String partName, XMLStreamWriter xmlWriter)
		throws XMLStreamException {
		xmlWriter.writeStartElement(partName);
	}

	private void writeEndElement(String partName, String prefix,
			XMLStreamWriter xmlWriter)  throws XMLStreamException {
		
		xmlWriter.writeEndElement();
		
	}
	
	private void writeEndElement(String partName, 
			XMLStreamWriter xmlWriter)  throws XMLStreamException {
		xmlWriter.writeEndElement();
	}
	
	private void writeText(String text, XMLStreamWriter xmlWriter)
		throws XMLStreamException {
		xmlWriter.writeCharacters(text);
	}
	
}
